<?php
namespace DRY\Render;

/**
 * DonReY Framework 2012 :: Render Transform - RSS 2.0 Feed generator
 * Generates a valid RSS 2.0 XML out of an array table of data + some header information.
 * This Transform adheres to the current RSS 2.0 Specification ( http://www.rssboard.org/rss-specification )
 *   Override this transform to provide more complex "field checking" methods and value defaults
 *
 * @todo Works, but needs more verifications for various elements
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/Render/xmlrss.html
 * @link http://www.rssboard.org/rss-specification
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Render
 */
class Transform_Xmlrss implements Transform_base
{

	const
		EXC_MISSING_FEED_ITEMS = 1,
		EXC_MISSING_REQUIRED_HEADER_ELEMENT = 2,
		EXC_INVALID_LANGUAGE_CODE = 3,
		EXC_INVALID_DATETIME_FORMAT = 4,
		EXC_MISSING_REQUIRED_ITEM_ELEMENT = 21
		;

	/**
	 * Methods to use to check header elements for correctness.
	 *   If array value is "true", it means that the element must exist without any specific format
	 *   If the array value is "false", it means the element is optional
	 * @var unknown_type
	 */
	private static $header_Check = array(
		'title'=> true,
		'link'=> "check_Link", // must be valid link. If not present, it defaults to the root site where this Transform is installed ( $_SERVER['SERVER_NAME'] )
		'description'=> true,
		'language'=> "check_Language",
		'copyright'=> false,
		'managingEditor'=> "check_Email",
		'webMaster'=> "check_Email",
		'pubDate'=> "check_Date",
		'lastBuildDate'=> "check_Date",
		'category'=> "check_Category",
		'generator'=> "check_Generator", // if missing it defaults to: DonReY Framework {@version}
		'docs'=> "check_Docs", // if missing it defaults to: http://www.rssboard.org/rss-specification
		'ttl'=> "check_Ttl",
		'image'=> "check_Image",
		'rating'=> "check_Rating",
//		'textInput'=> "check_TextInput", // Disabled because it's not used
		'skipHours'=> "check_SkipHours",
		'skipDays'=>" check_SkipDays"
	);

	private static $item_Check = array(
		'title'=> false,
		'link'=> "check_Link",
		'description'=> false, // note that either "title" or "description" must be present, so "description" is checked for to exist if Title did not
		'author'=> "check_Email",
		'category'=> "check_Category",
		'comments'=> "check_Link",
		'enclosure'=> "check_Enclosure",
		'guid'=> false,
		'pubDate'=> "check_Date",
		'source'=> "check_Source"
	);

	/**
	 * Configuration for this Transform (Reference)
	 * @var SimpleXMLElement
	 */
	protected $CFG;

	public $header = array();

	public $items = array();

	/**
	 * Combines configuration header with passed header, saving everything in $header (And also saves "Data")
	 *
	 * @param SimpleXMLElement $CFG
	 * @param array $args
	 */
	public function __construct(&$CFG, $args = array())
	{
		$this-> CFG = &$CFG;

		if(count($args) == 3) // header + items
			$header_values = $args[1];
		elseif(count($args) == 2) // just items, use only the default header parameters inside CFG
			$header_values = array();
		else
			throw new \DRY\Water('render.xmlrss', self::EXC_MISSING_FEED_ITEMS);

		// prepare header elements, in the proper order which is inside specifications, and use callbacks to check for their correctness
		foreach(self::$header_Check as $element_name=>$method) {

			// element data (passed parameter takes precedence to the config)
			if(isset($header_values[$element_name]))
				$element = $header_values[$element_name];
				elseif(isset($CFG-> defaults-> $element_name))
					$element = $CFG-> defaults-> $element_name;
				else
					$element = null;

			if($method === true && empty($element))
				throw new \DRY\Water('render.xmlrss', self::EXC_MISSING_REQUIRED_HEADER_ELEMENT, array('element'=>$element_name));

			if(is_string($method) && method_exists($this, $method)) {
				$this-> header[$element_name] = $this-> $method($element, true);
			} elseif(isset($element)) { // Basic string element, nothing special
				$this-> header[$element_name] = (string)$element;
			}
		}

		// check all Item elements (if they exist)
		foreach(count($args) == 3?$args[2]:$args[1] as $item_data) {
			// check if at least one of "description" or "title" is not empty, if both are, skip the item
			if(empty($item_data['title']) && empty($item_data['description']))
				continue;

			$item = array();
			foreach(self::$item_Check as $element_name=>$method) {
				// items do not have "defaults"
				if(isset($item_data[$element_name]))
					$element = $item_data[$element_name];
				else
					$element = null;

				if($method === true && empty($element))
					throw new \DRY\Water('render.xmlrss', self::EXC_MISSING_REQUIRED_ITEM_ELEMENT, array('element'=>$element_name));

				if(is_string($method) && method_exists($this, $method)) {
					$item[$element_name] = $this-> $method($element);
				} elseif(isset($element)) { // Basic string element, nothing special
					$item[$element_name] = (string)$element;
				}
			}
			$this-> items[] = $item;
		}
	}

//===== Data Checking Methods =====//

	protected function check_Link($element, $header_mode = false)
	{
		// TODO: check URL validity.
		if($header_mode)
			return empty($element)?"http://{$_SERVER['SERVER_NAME']}/":(string)$element;
		else {
			return (string)$element;
		}
	}

	/**
	 * List of allowed languages for the <language> element
	 * @var array
	 */
	private static $defined_languages = array(
		'Afrikaans'=> 'af', 'Albanian'=> 'sq', 'Basque'=> 'eu', 'Belarusian'=> 'be', 'Bulgarian'=> 'bg', 'Catalan'=> 'ca',
		'Chinese (Simplified)'=> 'zh-cn', 'Chinese (Traditional)'=> 'zh-tw', 'Croatian'=> 'hr', 'Czech'=> 'cs', 'Danish'=> 'da',
		'Dutch'=> 'nl', 'Dutch (Belgium)'=> 'nl-be', 'Dutch (Netherlands)'=> 'nl-nl', 'English'=> 'en', 'English (Australia)'=> 'en-au',
		'English (Belize)'=> 'en-bz', 'English (Canada)'=> 'en-ca', 'English (Ireland)'=> 'en-ie', 'English (Jamaica)'=> 'en-jm',
		'English (New Zealand)'=> 'en-nz', 'English (Phillipines)'=> 'en-ph', 'English (South Africa)'=> 'en-za', 'English (Trinidad)'=> 'en-tt',
		'English (United Kingdom)'=> 'en-gb', 'English (United States)'=> 'en-us', 'English (Zimbabwe)'=> 'en-zw', 'Estonian'=> 'et',
		'Faeroese'=> 'fo', 'Finnish'=> 'fi', 'French'=> 'fr', 'French (Belgium)'=> 'fr-be', 'French (Canada)'=> 'fr-ca', 'French (France)'=> 'fr-fr',
		'French (Luxembourg)'=> 'fr-lu', 'French (Monaco)'=> 'fr-mc', 'French (Switzerland)'=> 'fr-ch', 'Galician'=> 'gl', 'Gaelic'=> 'gd',
		'German'=> 'de', 'German (Austria)'=> 'de-at', 'German (Germany)'=> 'de-de', 'German (Liechtenstein)'=> 'de-li', 'German (Luxembourg)'=> 'de-lu',
		'German (Switzerland)'=> 'de-ch', 'Greek'=> 'el', 'Hawaiian'=> 'haw', 'Hungarian'=> 'hu', 'Icelandic'=> 'is', 'Indonesian'=> 'in',
		'Irish'=> 'ga', 'Italian'=> 'it', 'Italian (Italy)'=> 'it-it', 'Italian (Switzerland)'=> 'it-ch', 'Japanese'=> 'ja', 'Korean'=> 'ko',
		'Macedonian'=> 'mk', 'Norwegian'=> 'no', 'Polish'=> 'pl', 'Portuguese'=> 'pt', 'Portuguese (Brazil)'=> 'pt-br', 'Portuguese (Portugal)'=> 'pt-pt',
		'Romanian'=> 'ro', 'Romanian (Moldova)'=> 'ro-mo', 'Romanian (Romania)'=> 'ro-ro', 'Russian'=> 'ru', 'Russian (Moldova)'=> 'ru-mo',
		'Russian (Russia)'=> 'ru-ru', 'Serbian'=> 'sr', 'Slovak'=> 'sk', 'Slovenian'=> 'sl', 'Spanish'=> 'es', 'Spanish (Argentina)'=> 'es-ar',
		'Spanish (Bolivia)'=> 'es-bo', 'Spanish (Chile)'=> 'es-cl', 'Spanish (Colombia)'=> 'es-co', 'Spanish (Costa Rica)'=> 'es-cr',
		'Spanish (Dominican Republic)'=> 'es-do', 'Spanish (Ecuador)'=> 'es-ec', 'Spanish (El Salvador)'=> 'es-sv', 'Spanish (Guatemala)'=> 'es-gt',
		'Spanish (Honduras)'=> 'es-hn', 'Spanish (Mexico)'=> 'es-mx', 'Spanish (Nicaragua)'=> 'es-ni', 'Spanish (Panama)'=> 'es-pa',
		'Spanish (Paraguay)'=> 'es-py', 'Spanish (Peru)'=> 'es-pe', 'Spanish (Puerto Rico)'=> 'es-pr', 'Spanish (Spain)'=> 'es-es',
		'Spanish (Uruguay)'=> 'es-uy', 'Spanish (Venezuela)'=> 'es-ve', 'Swedish'=> 'sv', 'Swedish (Finland)'=> 'sv-fi',
		'Swedish (Sweden)'=> 'sv-se', 'Turkish'=> 'tr', 'Ukranian'=> 'uk'
	);

	protected function check_Language($element)
	{
		if(!empty($element))
			if(in_array((string)$element, self::$defined_languages))
				return (string)$element;
			else
				throw new \DRY\Water('render.xmlrss', self::EXC_INVALID_LANGUAGE_CODE, array('langc'=> (string)$element));
		else
			return 'en'; // defaults to English
	}

	/**
	 * Converts any date format supported by strtotime() to valid RSS 2.0 Date format (RFC 822)
	 * Enter description here ...
	 * @param unknown_type $element
	 */
	protected function check_Date($element)
	{
		if(!empty($element)) {
			if(($time = strtotime((string)$element))===false) {
				if(is_numeric($element))
					$time = (int)$element;
				else
					throw new \DRY\Water('render.xmlrss', self::EXC_INVALID_DATETIME_FORMAT, array('datetime'=> (string)$element));
			}
			return date('D, d M Y H:i:s O', $time);
		}
	}

	protected function check_Docs($element)
	{
		return empty($element)?"http://www.rssboard.org/rss-specification":(string)$element;
	}

	protected function check_Enclosure($element)
	{
		if(!empty($element) && is_array($element))
			return array('__attributes'=>$element);
	}


	/**
	 * Generates the RSS 2.0 stream
	 * @param XMLWriter $xmlw
	 */
	protected function render(XMLWriter $xmlw)
	{
		$xmlw-> startElement('rss');
		$xmlw-> writeAttribute('version', "2.0");
		$xmlw-> startElement('channel');

		// write channel "header" elements if they are not empty
		foreach($this-> header as $element_name=> $value) {
			if(!empty($value))
				if(is_array($value))
					\DRY\Module\XML::from_Array($value, $element_name);
				else {
					$xmlw-> startElement($element_name);
					\DRY\Module\XML::scalar_Content($value);
					$xmlw-> endElement();
				}
		}
		// write items
		foreach($this-> items as $item) {
			\DRY\Module\XML::from_Array($item, 'item', true); // skip empty
		}

		$xmlw-> endElement(); // channel
		$xmlw-> endElement(); // rss
	}

	/**
	 * @see DRY_Render_Transform_base::__toString()
	 */
	public function __toString()
	{
		$this-> render(\DRY\Module\XML::init_XMLWriter());
		return \DRY\Module\XML::write_XML();
	}

	public function to_File($filepath)
	{
		$this-> render(\DRY\Module\XML::init_XMLWriter($filepath));
		\DRY\Module\XML::write_XML();
	}

	public function to_Output($as_Download = true) // as_Download not used yet.
	{
		$this-> render(\DRY\Module\XML::init_XMLWriter('php://output'));
		header('Content-type: application/rss+xml');
		\DRY\Module\XML::write_XML();
	}
}